Firecrackerを利用したコンテナランタイムfirecracker-containerdを試してみる
CX事業本部@大阪の岩田です。
Firecrackerを使ったコンテナランタイムfirecracker-containerdを試したので、簡単にご紹介します。
環境
今回利用した環境です
- OS Ubuntu Server 18.04 LTS (HVM), SSD Volume Type - 64 ビット (x86) ami-04b9e92b5572fa0d1
- インスタンスタイプ i3.metal
- Firecracker v0.19.1
- firecracker-containerd コミットID:fe2ba1b6091a61173eb7bb5d8ec2c9ad7b15f98f
本当は前回構築したラズパイ4の環境で動かしたかったのですが、MicroVM内のエージェントとホスト側が通信できずうまくいきませんでした。また時間を見つけて再チャレンジしようと思います。
firecracker-containerdとは?
Docker等で利用されているコンテナランタイムcontainedを拡張し、FirecrackerのMicroVMにコンテナ環境を構築するソフトウェアです。通常のコンテナ型仮想化技術は、ざっくり言ってしまうとプロセスの分離で、ホストOSとカーネルを共有しています(Docker for MacやKata container等、話を広げていくとキリがないので通常のコンテナ型仮想化技術というぼやかした表現をしています)。そのためオーバーヘッドが小さいというメリットがありますが、
- ホストOSと同じカーネルしか使えない
- 仮想環境間の分離レベルが小さい
といったデメリットもあります。
firecracker-containerdを利用すると、FirecrackerのMicroVM内にコンテナを作成することが可能になります。FirecrackerのMicroVMは非常に高速に起動するため、オーバーヘッドが小さいというコンテナ型仮想化のメリットを維持しつつも、コンテナ間の高度な環境分離を実現することができます。こういったセキュリティ面のメリットから、Fargateの基盤でもセキュリティレベルを向上させるためにfirecracker-containerdが採用されていることが知られています。
[レポート] AWS Fargate under the hood #reinvent #CON423]
Lambdaの基盤でfirecracker-containerdが採用されているという明示的なドキュメントは見つけられませんでしたが、LambdaのアーキテクチャとしてFirecrackerのMicroVM上にサンドボックス環境(コンテナ)を構築することは知られており、Lambdaの基盤でも恐らくfirecracker-containerdが利用されているのかなー?と想像しています。
こちらは昨年末のre:invent2019でのfirecracker-containerdに関するセッションです
こちらはdocker con19でのfirecracker-containerdに関するセッションです
環境構築
ここからは実際に環境構築の手順をご紹介します。EC2インスタンスを起動後に諸々のセットアップを行います。
Dockerのインストール
諸々のビルドにビルド用のコンテナを利用するので、Dockerをインストールしておきます。
$ sudo apt update $ sudo apt install -y docker.io $ sudo systemctl start docker $ sudo usermod -aG docker $USER
ubuntuユーザーをdockerグループに追加したので、一旦ログアウトと再ログインを行いましょう。
Goのインストール
続いてGoをインストールします。firecracker-containerdにはGoの11.0以後が必要ですが、普通にaptでインストールするとGoのバージョンが古いので、リポジトリを追加してからインストールします。
$ sudo add-apt-repository ppa:longsleep/golang-backports $ sudo apt update $ sudo apt install -y golang-go
インストールできたらバージョンを確認しましょう
$ go version go version go1.13.4 linux/amd64
firecracker-containerdと関連ツールのインストール
ここからが本番です。firecracker-containerdのソースコードを取得して諸々ビルド&インストールしていきましょう。
$ git clone --recurse-submodules https://github.com/firecracker-microvm/firecracker-containerd $ cd firecracker-containerd/ $ make all $ sudo make install
これでfirecracker-containerdが/usr/local/binにインストールされます。
続いてFirecracker本体もインストールします。先程git cloneした際にサブモジュールとしてFirecrackerのソースもクローンされています。
$ sudo make install-firecracker
ビルド用のコンテナが起動し、FIrecrackerとJailerのバイナリがビルド&インストールされます。
続いてfirecracker-containerd用のディレクトリや設定ファイルを準備します。
$ sudo mkdir -p /etc/firecracker-containerd $ sudo mkdir -p /var/lib/firecracker-containerd/containerd $ sudo mkdir -p /var/lib/firecracker-containerd/snapshotter/devmapper $ sudo tee /etc/firecracker-containerd/config.toml <<EOF disabled_plugins = ["cri"] root = "/var/lib/firecracker-containerd/containerd" state = "/run/firecracker-containerd" [grpc] address = "/run/firecracker-containerd/containerd.sock" [plugins] [plugins.devmapper] pool_name = "fc-dev-thinpool" base_image_size = "10GB" root_path = "/var/lib/firecracker-containerd/snapshotter/devmapper" [debug] level = "debug" EOF
snapshotter用のプールを作成するために以下のシェルスクリプトを実行します
#!/bin/bash # Sets up a devicemapper thin pool with loop devices in # /var/lib/firecracker-containerd/snapshotter/devmapper set -ex DIR=/var/lib/firecracker-containerd/snapshotter/devmapper POOL=fc-dev-thinpool if [[ ! -f "${DIR}/data" ]]; then touch "${DIR}/data" truncate -s 100G "${DIR}/data" fi if [[ ! -f "${DIR}/metadata" ]]; then touch "${DIR}/metadata" truncate -s 2G "${DIR}/metadata" fi DATADEV="$(losetup --output NAME --noheadings --associated ${DIR}/data)" if [[ -z "${DATADEV}" ]]; then DATADEV="$(losetup --find --show ${DIR}/data)" fi METADEV="$(losetup --output NAME --noheadings --associated ${DIR}/metadata)" if [[ -z "${METADEV}" ]]; then METADEV="$(losetup --find --show ${DIR}/metadata)" fi SECTORSIZE=512 DATASIZE="$(blockdev --getsize64 -q ${DATADEV})" LENGTH_SECTORS=$(bc <<< "${DATASIZE}/${SECTORSIZE}") DATA_BLOCK_SIZE=128 # see https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt LOW_WATER_MARK=32768 # picked arbitrarily THINP_TABLE="0 ${LENGTH_SECTORS} thin-pool ${METADEV} ${DATADEV} ${DATA_BLOCK_SIZE} ${LOW_WATER_MARK} 1 skip_block_zeroing" echo "${THINP_TABLE}" if ! $(dmsetup reload "${POOL}" --table "${THINP_TABLE}"); then dmsetup create "${POOL}" --table "${THINP_TABLE}" fi
firecracker-containerdを利用するにはMicroVM内で専用のエージェントを動作させる必要があります。以下のコマンドでエージェント導入済みのルートファイルシステムを準備します。
$ make image
準備できたら先程作成した設定ファイルで指定した場所に配置します。
$ sudo mkdir -p /var/lib/firecracker-containerd/runtime $ sudo mv tools/image-builder/rootfs.img /var/lib/firecracker-containerd/runtime/default-rootfs.img
続いてMicroVM用のカーネルも取得&配置します。
$ curl -fsSL -o hello-vmlinux.bin https://s3.amazonaws.com/spec.ccfc.min/img/hello/kernel/hello-vmlinux.bin $ sudo mv hello-vmlinux.bin /var/lib/firecracker-containerd/runtime/default-vmlinux.bin
containerd用の設定ファイルを準備します
sudo mkdir /etc/containerd/ sudo tee /etc/containerd/firecracker-runtime.json <<EOF { "firecracker_binary_path": "/usr/local/bin/firecracker", "cpu_template": "T2", "log_fifo": "fc-logs.fifo", "log_level": "Debug", "metrics_fifo": "fc-metrics.fifo", "kernel_args": "console=ttyS0 noapic reboot=k panic=1 pci=off nomodules ro systemd.journald.forward_to_console systemd.unit=firecracker.target init=/sbin/overlay-init", "default_network_interfaces": [{ "CNIConfig": { "NetworkName": "fcnet", "InterfaceName": "veth0" } }] } EOF
これで準備完了です!
やってみる
実際に動かしてみます。まずfirecracker-containerdのプロセスを起動します。
$ sudo firecracker-containerd --config /etc/firecracker-containerd/config.toml
起動すると以下のように出力されます。
INFO[2020-01-19T11:49:24.304547210Z] starting containerd revision= version=1.3.1+unknown INFO[2020-01-19T11:49:24.347480676Z] loading plugin "io.containerd.content.v1.content"... type=io.containerd.content.v1 INFO[2020-01-19T11:49:24.347586050Z] loading plugin "io.containerd.snapshotter.v1.devmapper"... type=io.containerd.snapshotter.v1 INFO[2020-01-19T11:49:24.347673176Z] initializing pool device "fc-dev-thinpool" INFO[2020-01-19T11:49:24.350407561Z] using dmsetup: Library version: 1.02.145 (2017-11-03) Driver version: 4.37.0 INFO[2020-01-19T11:49:24.357838236Z] loading plugin "io.containerd.snapshotter.v1.overlayfs"... type=io.containerd.snapshotter.v1 INFO[2020-01-19T11:49:24.358035986Z] loading plugin "io.containerd.metadata.v1.bolt"... type=io.containerd.metadata.v1 INFO[2020-01-19T11:49:24.358085041Z] metadata content store policy set policy=shared INFO[2020-01-19T11:49:24.358273619Z] loading plugin "io.containerd.differ.v1.walking"... type=io.containerd.differ.v1 INFO[2020-01-19T11:49:24.358317002Z] loading plugin "io.containerd.gc.v1.scheduler"... type=io.containerd.gc.v1 INFO[2020-01-19T11:49:24.358373313Z] loading plugin "io.containerd.service.v1.containers-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358403712Z] loading plugin "io.containerd.service.v1.content-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358430548Z] loading plugin "io.containerd.service.v1.diff-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358464903Z] loading plugin "io.containerd.service.v1.images-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358492383Z] loading plugin "io.containerd.service.v1.leases-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358518056Z] loading plugin "io.containerd.service.v1.namespaces-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358544418Z] loading plugin "io.containerd.service.v1.snapshots-service"... type=io.containerd.service.v1 INFO[2020-01-19T11:49:24.358569857Z] loading plugin "io.containerd.runtime.v1.linux"... type=io.containerd.runtime.v1 INFO[2020-01-19T11:49:24.358648695Z] loading plugin "io.containerd.runtime.v2.task"... type=io.containerd.runtime.v2 DEBU[2020-01-19T11:49:24.358741889Z] loading tasks in namespace namespace=default WARN[2020-01-19T11:49:24.358965429Z] cleaning up after shim disconnected id=test namespace=default INFO[2020-01-19T11:49:24.358990465Z] cleaning up dead shim ...略
別のシェルからdebianのコンテナイメージを取得してみましょう
sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ image pull \ --snapshotter devmapper \ docker.io/library/debian:latest
イメージが準備できたらコンテナの起動です!
sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ run \ --snapshotter devmapper \ --runtime aws.firecracker \ --rm --tty --net-host \ docker.io/library/debian:latest \ debian-test
コンテナを起動すると、先程firecracker-containerdを起動したシェルの方に以下のようなログが流れてきます。
DEBU[2020-01-19T11:50:06.673987018Z] stat snapshot key="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:06.674063559Z] stat key="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:06.679773978Z] prepare snapshot key=test parent="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:06.767683266Z] garbage collected d=1.617186ms DEBU[2020-01-19T11:50:34.089003443Z] stat snapshot key="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:34.089074135Z] stat key="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:34.093979570Z] prepare snapshot key=debian-test parent="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:34.094128580Z] prepare key=default/8/debian-test parent="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0" DEBU[2020-01-19T11:50:34.094277581Z] creating snapshot device 'fc-dev-thinpool-snap-6' from 'fc-dev-thinpool-snap-4' DEBU[2020-01-19T11:50:34.228530373Z] event published ns=default topic=/snapshot/prepare type=containerd.events.SnapshotPrepare DEBU[2020-01-19T11:50:34.233751587Z] get snapshot mounts key=debian-test DEBU[2020-01-19T11:50:34.233833659Z] mounts key=default/8/debian-test DEBU[2020-01-19T11:50:34.276741948Z] event published ns=default topic=/containers/create type=containerd.events.ContainerCreate DEBU[2020-01-19T11:50:34.280182423Z] get snapshot mounts key=debian-test DEBU[2020-01-19T11:50:34.280251418Z] mounts key=default/8/debian-test time="2020-01-19T11:50:34.319317129Z" level=debug msg=StartShim runtime=aws.firecracker task_id=debian-test time="2020-01-19T11:50:34.319581654Z" level=info msg="will start a single-task VM since no VMID has been provided" runtime=aws.firecracker task_id=debian-test vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd DEBU[2020-01-19T11:50:34.320383965Z] create VM request: VMID:"526a6692-d0ba-4b24-a5ef-a61434d2b1cd" ContainerCount:1 ExitAfterAllTasksDeleted:true DEBU[2020-01-19T11:50:34.320418154Z] using namespace: default DEBU[2020-01-19T11:50:34.320664216Z] starting containerd-shim-aws-firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd DEBU[2020-01-19T11:50:34.343828149Z] garbage collected d=1.784911ms INFO[2020-01-19T11:50:34.361581942Z] starting signal loop namespace=default path=/var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd pid=44088 INFO[2020-01-19T11:50:34.361843231Z] creating new VM runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.519397998Z] Called startVMM(), setting up a VMM on firecracker.sock runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.532714107Z] refreshMachineConfiguration: [GET /machine-config][200] getMachineConfigurationOK &{CPUTemplate:T2 HtEnabled:0xc00039452b MemSizeMib:0xc000394520 VcpuCount:0xc000394518} runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.533313802Z] PutGuestBootSource: [PUT /boot-source][204] putGuestBootSourceNoContent runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.533355817Z] Attaching drive /var/lib/firecracker-containerd/runtime/default-rootfs.img, slot root_drive, root true. runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.534035946Z] Attached drive /var/lib/firecracker-containerd/runtime/default-rootfs.img: [PUT /drives/{drive_id}][204] putGuestDriveByIdNoContent runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.534077231Z] Attaching drive /var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd/ctrstub0, slot MN2HE43UOVRDA, root false. runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd INFO[2020-01-19T11:50:34.534598845Z] Attached drive /var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd/ctrstub0: [PUT /drives/{drive_id}][204] putGuestDriveByIdNoContent runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd ...略
Firecrackerの諸々のAPIを実行してMicroVMを起動していることが読み取れますね。コンテナを起動したシェルを確認すると、無事debianのコンテナが起動したことが分かります。
root@microvm:/#
ここからは通常のcontainerdとfirecracker-containerdを比較してみます。まず通常のcontainerdを使ってDockerHubで公開されているnmeyerhans/stress:latest
のイメージを実行してみます。
$ sudo ctr image pull docker.io/nmeyerhans/stress:latest $ sudo ctr run --tty --env CPU_THREADS=2 --env IO_THREADS=4 docker.io/nmeyerhans/stress:latest $(uuid)
この状態でpgrep stress
を実行するといくつかプロセスIDが表示されます。
$ pgrep stress 44676 44678 44679 44680 44681 44682 44683
続いてtopコマンドの出力結果です。
$ top -b n1|head -n 15 top - 13:47:54 up 18 min, 3 users, load average: 2.39, 1.35, 1.02 Tasks: 700 total, 4 running, 352 sleeping, 0 stopped, 0 zombie %Cpu(s): 1.6 us, 0.5 sy, 0.0 ni, 97.9 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 52824844+total, 52084355+free, 1026088 used, 6378792 buff/cache KiB Swap: 0 total, 0 free, 0 used. 52415888+avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 42372 root 20 0 7276 84 0 R 100.0 0.0 0:23.42 stress 42370 root 20 0 7276 84 0 R 94.4 0.0 0:23.41 stress 42375 root 20 0 7276 84 0 D 50.0 0.0 0:10.79 stress 42371 root 20 0 7276 84 0 R 44.4 0.0 0:10.78 stress 42373 root 20 0 7276 84 0 D 44.4 0.0 0:10.64 stress 42374 root 20 0 7276 84 0 D 44.4 0.0 0:10.76 stress 1508 root 0 -20 0 0 0 I 11.1 0.0 0:02.37 kworker/57:1H 42382 ubuntu 20 0 43316 4516 3396 R 11.1 0.0 0:00.04 top
これらの出力結果から、通常のcontainerdにおけるコンテナはあくまでプロセスであることが分かります。続いてfirecracker-containerdを使ってコンテナを実行するパターンです。
$ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ image pull \ --snapshotter devmapper \ docker.io/nmeyerhans/stress:latest $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ run --tty --env CPU_THREADS=2 --env IO_THREADS=4 \ --snapshotter devmapper \ --runtime aws.firecracker \ docker.io/nmeyerhans/stress:latest $(uuid)
先程と同様pgrepしてみます
$ pgrep stress $ pgrep firecracker 42411 42449 42605
今度はstress
という文字列でpgrepしても何もヒットしません。代わりにfirecracker
でpgrepすると、いくつかのプロセスがヒットします。firecracker
プロセスの内部でMicroVMが起動し、MicroVM内でコンテナが実行されているためです。同様にtopコマンドの出力結果です。
$ top -b n1|head -n 15 top - 13:49:24 up 19 min, 3 users, load average: 1.75, 1.50, 1.10 Tasks: 699 total, 1 running, 351 sleeping, 0 stopped, 0 zombie %Cpu(s): 1.5 us, 0.5 sy, 0.0 ni, 97.9 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 52824844+total, 52071091+free, 1042564 used, 6494980 buff/cache KiB Swap: 0 total, 0 free, 0 used. 52403798+avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 42605 root 20 0 138032 109844 109204 S 117.6 0.0 0:58.42 firecracker 42613 ubuntu 20 0 43316 4508 3388 R 11.8 0.0 0:00.04 top 5146 root 20 0 0 0 0 I 5.9 0.0 0:01.78 kworker/25:2 1 root 20 0 225468 9236 6692 S 0.0 0.0 0:03.72 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd 3 root 20 0 0 0 0 I 0.0 0.0 0:01.11 kworker/0:0 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H 5 root 20 0 0 0 0 I 0.0 0.0 0:00.08 kworker/u144:0
こちらもホストOSからはfirecrackerのプロセスしか見えていないことが分かります。
まとめ
firecracker-containerdのご紹介でした。このあたりの技術を深堀りしていくことで、Lambdaの裏側に詳しくなれそうです。普段の開発では比較的高レイヤしか意識する機会が無いのですが、もうちょっと低レイヤーな技術も頑張って勉強していきたいと思います。